#!/usr/bin/perl

use strict;
use warnings;

use Data::Dumper;
use OpenGL qw/ :all :old /;

use constant WIDTH => 400;
use constant HEIGHT => 400;
use constant DISK_HEIGHT => 20;
use constant HANOI_SOLVE => 0;
use constant HANOI_QUIT => 1;
use constant HANOI_LIGHTING => 2;
use constant HANOI_WALL => 3;
use constant HANOI_FOG => 4;

my $motion = GL_TRUE;
my $back_wall = GL_FALSE;
my $xangle = 0; my $yangle = 0;
my $xlangle = 0; my $ylangle = 0;

my $wallz = -(WIDTH/2);

my $NUM_DISKS = 3;
my $CONE = $NUM_DISKS + 1;

my @lightOneDirection = (0, 0, -1);
my @lightOnePosition = (200, 100, 300, 1);
my @lightOneColor = (1.0, 1.0, 0.5, 1.0);

my @lightTwoDirection = (0, 0, -1);
my @lightTwoPosition = (600, 100, 300, 1);
my @lightTwoColor = (1.0, 0.0, 0.3, 1.0);

my @lightZeroPosition = (400, 200, 300, 1);
my @lightZeroColor = (0.3, 0.3, 0.3, 0.3);

my @diskColor = (1.0, 1.0, 1.0, 0.8); 
my @poleColor = (1.0, 0.2, 0.2, 0.8);

my @poles = {depth => 0, head => undef};

sub _push {
    my ($which, $size) = @_;
    my $new = {size => $size, next => undef};
    $new->{next} = $poles[$which]->{head};
    $poles[$which]->{head} = $new;
    $poles[$which]->{depth}++;
}

sub _pop {
    my $which = shift;
    my $retval = $poles[$which]->{head}->{size};
    $poles[$which]->{head} = $poles[$which]->{head}->{next};
    $poles[$which]->{depth}--;
    return $retval;
}

my $moves = {depth => 0, head => undef, tail => undef};

sub _mpop {
    $moves->{head} = $moves->{head}->{next};
    $moves->{depth}--;
}

sub _mpush {
    my ($t, $f) = @_;
    my $new = {t => $t, f => $f, next => undef, prev => undef};
    $new->{prev} = $moves->{tail};
    $moves->{tail}->{next} = $new if $moves->{tail};
    $moves->{tail} = $new;
    $moves->{head} = $moves->{tail} unless $moves->{head};
    $moves->{depth}++;
}

sub init {
    for (my $i = 1; $i <= $NUM_DISKS; $i++) {
        glNewList($i, GL_COMPILE);
        {
            glutSolidTorus(DISK_HEIGHT / 2, 5 * $i, 15, 15);
        }
        glEndList();
    }
    glNewList($CONE, GL_COMPILE);
    {
        glutSolidCone(10, ($NUM_DISKS + 1) * DISK_HEIGHT, 20, 20);
    }
    glEndList();
}

sub keyboard {
    my ($key, undef, undef) = @_;
    if ($key == 27 || $key =~ /^q$/i) {
        exit(0);
    }
}

sub update {
    glutPostRedisplay();
}

sub DrawPost {
    my $xcenter = shift;
    glPushMatrix();
    {
        glTranslatef($xcenter, 0, 0);
        glRotatef(90, -1, 0, 0);
        glCallList($CONE);
    }
    glPopMatrix();
}

sub DrawPosts {
    glColor3fv_p(@poleColor[0..2]);
    glLineWidth(10);
    glMaterialfv_p(GL_FRONT, GL_DIFFUSE, @poleColor);
    DrawPost(WIDTH / 4);
    DrawPost(2 * WIDTH / 4);
    DrawPost(3 * WIDTH / 4);
}

sub DrawDisk {
    my ($xcenter, $ycenter, $size) = @_;
    glPushMatrix();
    {
        glTranslatef($xcenter, $ycenter, 0);
        glRotatef(90, 1, 0, 0);
        glCallList($size);
    } 
    glPopMatrix();
}

sub DrawDooDads {
    my ($xcenter, $ycenter);
    glColor3fv_p(@diskColor[0..2]);
    glMaterialfv_p(GL_FRONT, GL_DIFFUSE, @diskColor);
    for (my $i = 1; $i <= 3; $i++) {
        $xcenter = $i * WIDTH / 4;
        next unless $poles[$i];
        for 
        (
            my $temp = $poles[$i]->{head},
                $ycenter = DISK_HEIGHT * $poles[$i]->{depth} - DISK_HEIGHT / 2;
            $temp;
            $temp = $temp->{next}, $ycenter -= DISK_HEIGHT
        )
        {
            DrawDisk($xcenter, $ycenter, $temp->{size});
        }
    }
}

sub mov {
    my ($n, $f, $t) = @_;
    if ($n == 1) {
        _mpush(($t),($f));
        return;
    }
    my $o = (6-($f + $t));
    mov($n - 1, $f, $o);
    mov(1, $f, $t);
    mov($n - 1, $o, $t);
}

my @wallcolor = (0, 0.3, 1, 1);

sub DrawWall {
    glColor3fv_p(@wallcolor[0..2]);
    for (my $i = 0; $i < WIDTH; $i += 10) {
        for (my $j = 0; $j < HEIGHT; $j += 10) {
            glBegin(GL_POLYGON);
            {
                glNormal3f(0, 0, 1);
                glVertex3f($i + 10, $j, $wallz);
                glVertex3f($i + 10, $j + 10, $wallz);
                glVertex3f($i, $j + 10, $wallz);
                glVertex3f($i, $j, $wallz);
            }
            glEnd();
        }
    }
}

sub draw {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    if ($back_wall) {
        glMaterialfv_p(GL_FRONT, GL_DIFFUSE, @wallcolor);
        DrawWall();
    }
    glPushMatrix();
    {
        glTranslatef(WIDTH / 2, HEIGHT / 2, 0);
        glRotatef($xlangle, 0, 1, 0);
        glRotatef($ylangle, 1, 0, 0);
        glTranslatef(-(WIDTH) / 2, -(HEIGHT) / 2, 0);
        glLightfv_p(GL_LIGHT0, GL_POSITION, @lightZeroPosition);
    }
    glPopMatrix();

    glPushMatrix();
    {
        glTranslatef(WIDTH / 2, HEIGHT / 2, 0);
        glRotatef($xangle, 0, 1, 0);
        glRotatef($yangle, 1, 0, 0);
        glTranslatef(-(WIDTH) / 2, -(HEIGHT) / 2, 0);
        DrawPosts();
        DrawDooDads();
    }
    glPopMatrix();
    if ($motion && $moves->{depth}) {
        my $t = $moves->{head}->{t};
        my $f = $moves->{head}->{f};
        _push($t, _pop($f));
        _mpop();
    }
    glutSwapBuffers();
}

sub hanoi_menu {
    my $value = shift;
    if ($value == HANOI_SOLVE) {
        $motion = !$motion;
        if ($motion) {
            glutIdleFunc(\&update);
        } else {
            glutIdleFunc(undef);
        }
    }
    elsif ($value == HANOI_LIGHTING) {
        if (glIsEnabled(GL_LIGHTING)) {
            glDisable(GL_LIGHTING);
        }
        else {
            glEnable(GL_LIGHTING);
        }
    }
    elsif ($value == HANOI_WALL) {
        $back_wall = !$back_wall;
    }
    elsif ($value == HANOI_FOG) {
        if (glIsEnabled(GL_FOG)) {
            glDisable(GL_FOG);
        }
        else {
            glEnable(GL_FOG);
            glFogi(GL_FOG_MODE, GL_EXP);
            glFogf(GL_FOG_DENSITY, 0.01);
        }
    }
    elsif ($value == HANOI_QUIT) {
        exit(0);
    }
    glutPostRedisplay();
}

my ($oldx, $oldy);
my $leftb = GL_FALSE; my $middleb = GL_FALSE;

sub hanoi_mouse {
    my ($button, $state, $x, $y) = @_;
    if ($button == GLUT_LEFT_BUTTON) {
        $oldx = $x;
        $oldy = $y;
        if ($state == GLUT_DOWN) {
            $leftb = GL_TRUE;
        }
        else {
            $leftb = GL_FALSE;
        }
    }
    if ($button == GLUT_MIDDLE_BUTTON) {
        $oldx = $x;
        $oldy = $y;
        if ($state == GLUT_DOWN) {
            $middleb = GL_TRUE;
        }
        else {
            $middleb = GL_FALSE;
        }
    }
}

sub hanoi_visibility {
    my $state = shift;
    if ($state == GLUT_VISIBLE && $motion) {
        glutIdleFunc(\&update);
    } else {
        glutIdleFunc(undef);
    }
}

sub hanoi_motion {
    my ($x, $y) = @_;
    if ($leftb) {
        $xangle -= ($x - $oldx);
        $yangle -= ($y - $oldy);
    }
    if ($middleb) {
        $xlangle -= ($x - $oldx);
        $ylangle -= ($y - $oldy);
    }
    $oldx = $x;
    $oldy = $y;
    glutPostRedisplay();
}

sub main {
    glutInit();
    glutInitWindowSize(WIDTH, HEIGHT);
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);

    glutCreateWindow("Hanoi");

    glutDisplayFunc(\&draw);
    glutKeyboardFunc(\&keyboard);

    glViewport(0, 0, WIDTH, HEIGHT);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, WIDTH, 0, HEIGHT, -10000, 10000);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glClearColor(0, 0, 0, 0);
    glClearDepth(1.0);

    glEnable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);

#  /*  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, 1);  */

    glLightfv_p(GL_LIGHT1, GL_POSITION, @lightOnePosition);
    glLightfv_p(GL_LIGHT1, GL_DIFFUSE, @lightOneColor);
    glLightf(GL_LIGHT1, GL_SPOT_CUTOFF, 10);
    glLightfv_p(GL_LIGHT1, GL_SPOT_DIRECTION, @lightOneDirection);
    glEnable(GL_LIGHT1);

    glLightfv_p(GL_LIGHT2, GL_POSITION, @lightTwoPosition);
    glLightfv_p(GL_LIGHT2, GL_DIFFUSE, @lightTwoColor);
#  /*  glLightf(GL_LIGHT2,GL_LINEAR_ATTENUATION,.005); */
    glLightf(GL_LIGHT2, GL_SPOT_CUTOFF, 10);
    glLightfv_p(GL_LIGHT2, GL_SPOT_DIRECTION, @lightTwoDirection);
    glEnable(GL_LIGHT2);

    glLightfv_p(GL_LIGHT0, GL_DIFFUSE, @lightZeroColor);
    glEnable(GL_LIGHT0);

    glEnable(GL_LIGHTING);

    glutMouseFunc(\&hanoi_mouse);
    glutMotionFunc(\&hanoi_motion);
    glutVisibilityFunc(\&hanoi_visibility);

    glutCreateMenu(\&hanoi_menu);
    glutAddMenuEntry("Solve", HANOI_SOLVE);
    glutAddMenuEntry("Lighting", HANOI_LIGHTING);
    glutAddMenuEntry("Back Wall", HANOI_WALL);
    glutAddMenuEntry("Fog", HANOI_FOG);
    glutAddMenuEntry("Quit", HANOI_QUIT);
    glutAttachMenu(GLUT_RIGHT_BUTTON);
    init();

    for (my $i = 0; $i < $NUM_DISKS; $i++) {
        _push(1, $NUM_DISKS - $i);
    }
    mov($NUM_DISKS, 1, 3);
#    print Dumper(@poles);

    glutMainLoop();
}

&main;

